/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
using Nvigorate.Common;
using Nvigorate.Common.Reflection;
using Nvigorate.Extensions;
using Nvigorate.Relational.Mapping;
using Nvigorate.Relational.Mapping.Index;
using Nvigorate.Relational.Mapping.Interface;
using Nvigorate.Relational.Record;
using Nvigorate.Relational.State;
using Nvigorate.Relational.Visitors;

namespace Nvigorate.Relational.Repository
{
    public class PersistableTypeBinder<T> : TypeVisitorBase, ITypeVisitor
    {
        protected Dictionary<Tuple<Type,IRecord>, object> _instanceDictionary = new Dictionary<Tuple<Type, IRecord>, object>();
        protected List<Type> _subjectsProcessed = new List<Type>();
        protected DynamicAccessorCache _accessor = new DynamicAccessorCache();
        
        protected IRepositoryMapIndex RepositoryMapIndex { get; set; }
        protected DataSet SourceDataSet { get; set; }
        protected bool _relationshipsEstablished = false;
        
        protected DataRecordCollection GetRecordCollection(Type type, string suffix)
        {
            return SourceDataSet.Tables[type.Name + suffix];
        }

        public IEnumerable<T> Instances
        {
            get
            {
                if(!_relationshipsEstablished)
                {
                    BuildRelationships();
                    _relationshipsEstablished = true;
                }

                return _instanceDictionary
                    .Where(p => p.Key.Value1.Equals(typeof(T)))
                    .Select(p => p.Value)
                    .Cast<T>();
            }
        }

        public void Visit(Type type)
        {
            Visit(type, "");
        }

        public void Visit(ITargetRelationship relative, string suffix)
        {
            _visited.Add(relative);
            Visit(relative.RelativeType, relative.RelativeProperty);
        }
        
        protected void Visit(Type type, string suffix)
        {
            var map = RepositoryMapIndex.GetMap(type);
            if (SourceDataSet.Tables[type.Name + suffix] == null)
                return;

            var records = GetRecordCollection(type, suffix);
            var assignments = map.GetSuppliedAssignments(records[0]);

            records
                .ForEach(r =>
                    {
                        var instance = map.Discriminator != null ? CreateNewInstance(map.Discriminator.GetAppropriateType(r)) : CreateNewInstance(type);
                        if (
                                (
                                    instance.GetType().Equals(type) ||
                                    Reflector.InheritsFrom(instance.GetType(), type) ||
                                    Reflector.HasBaseTypeOf(instance.GetType(), type)
                                ) &&
                                !_instanceDictionary.ContainsKey(Tuple.Create(type, r))
                            )
                                
                        {
                            assignments
                                .Where(a => !a.IsConstructorArgument && (!a.Target.Field.Contains(".") || r[a.Target] != DBNull.Value))
                                .ForEach(a => _accessor.Write(instance, a.Target.Field, r[a.Target]));
                            _instanceDictionary.Add(Tuple.Create(type, r), instance);

                            //handle constructed types
                            assignments
                                .Where(a => a.IsConstructorArgument)
                                .GroupBy(a => a.ConstructedType, a => a)
                                .ForEach(g =>
                                    {
                                        var constructedType = g.Key;
                                        var argumentList = g.OrderBy(a => a.ArgumentIndex);
                                        var argumentValues = argumentList.Select(a => r[a.Target]);
                                        var argumentTypes = argumentValues.Select(v => v.GetType());
                                    });


                            PostBinding(instance, map);
                        }
                    });
        }
        
        protected object CreateNewInstance(Type type)
        {
            var instance = Activator.CreateInstance(type);

            if(instance is ITrackState)
            {
                var tracked = instance as ITrackState;
                tracked.Deserializing = true;
            }

            return instance;
        }

        protected void ConstructDataSet(Type targetType, string suffix, DataSet set)
        {
            if(_subjectsProcessed.Contains(targetType))
                return;

            _subjectsProcessed.Add(targetType);

            Func<string, string, string, string> relationshipName = (parent, child, property) => string.Format("{0}to{1}in{2}", parent, child, property);
            
            var map = RepositoryMapIndex.GetMap(targetType);
            foreach(var relationship in map.Relationships.OfType<OneToManyRelationship>())
            {
                var relativeTypeName =
                    relationship.RelativeType.Name;
                var subjectTypeName = targetType.Name;
                var parentTable =
                    set.Tables[subjectTypeName];
                var childTable =
                    set.Tables[relativeTypeName+relationship.RelativeProperty];

                var parentRelationshipName = relationshipName(subjectTypeName, relativeTypeName, relationship.RelativeProperty);
                var childRelationshipName = relationshipName(relativeTypeName, subjectTypeName, relationship.RelativeProperty);

                if (childTable != null)
                {
                    var relativeMap =
                        RepositoryMapIndex.GetMap(relationship.RelativeType);

                    var parentColumns = relationship
                        .Constraints
                        .Select(constraint =>
                            {
                                var assignment = map.GetAssignmentsByTargetField(constraint.Key).FirstOrDefault();
                                //var columnName = string.Format("{0}.{1}",
                                //                               assignment.Source.Entity + suffix,
                                //                               assignment.Source.Field);
                                return parentTable.Columns[assignment.Target.Field];
                            }).ToArray();

                    var childColumns = relationship
                        .Constraints
                        .Select(constraint =>
                            {
                                var assignment = relativeMap.GetAssignmentsByTargetField(constraint.Value).FirstOrDefault();
 
                                return childTable.Columns[assignment.Target.Field];
                            }).ToArray();

                    try
                    {
                        if (set.Relations[parentRelationshipName] == null)
                            set.Relations.Add(parentRelationshipName, parentColumns, childColumns, false);
                        //if(set.Relations[childRelationshipName] == null)
                        //    set.Relations.Add(childRelationshipName, childColumns, parentColumns, false);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e);
                    }

                    ConstructDataSet(relationship.RelativeType, relationship.RelativeProperty, set);
                }
            }

            foreach(var relationship in map.Relationships.OfType<ManyToManyRelationship>())
            {
                var relativeTypeName = relationship.RelativeType.Name;
                var subjectTypeName = targetType.Name;
                var parentTable = set.Tables[subjectTypeName];
                var childTable = set.Tables[relativeTypeName+relationship.RelativeProperty];
                var parentRelationshipName = relationshipName(subjectTypeName, relativeTypeName, relationship.RelativeProperty);

                if (childTable != null)
                {
                    var relativeMap = RepositoryMapIndex.GetMap(relationship.RelativeType);

                    var parentColumns = relationship
                        .Container
                        .SubjectPairs
                        .Select(pair =>
                            {
                                var assignment = map.GetAssignmentsByTargetField(pair.Target).FirstOrDefault();
                                var columnName = string.Format("{0}.{1}", 
                                    assignment.Source.Entity + suffix, 
                                    assignment.Source.Field);
                                return parentTable.Columns[columnName];
                            }).ToArray();

                    var childColumns = relationship
                        .Container
                        .SubjectPairs
                        .Select(pair =>
                            {
                                var assignment = map.GetAssignmentsByTargetField(pair.Target).FirstOrDefault();
                                var columnName = string.Format("{0}.{1}", 
                                    relationship.Container.ContainerName + relationship.RelativeProperty, 
                                    assignment.Source.Field);
                                return childTable.Columns[columnName];
                            }).ToArray();

                    set.Relations.Add(parentRelationshipName, parentColumns, childColumns, false);
                }
            }
        }

        protected void BuildRelationships()
        {
            _instanceDictionary
                .Where(p => p.Key.Value1.Equals(typeof(T)))
                .ForEach(i => PopulateChildProperties(i.Value, i.Key.Value2, new List<Type>()));
        }

        protected object GetChildPropertyValue(Type type, Type domainType, IRecord record, string property, List<Type> inheritenceStack)
        {
            var propertyType = Reflector.GetMemberType(type, property);
            var relationship = string.Format("{0}to{1}in{2}", type.Name, domainType.Name, property);
            var childRecords = record.GetRelatedRecords(relationship);

            if(childRecords.Count == 0)
            {
                relationship = string.Format("{0}to{1}", property, type.Name);
                childRecords = record.GetRelatedRecords(relationship);
            }

            if(childRecords != null && childRecords.Count > 0)
            {
                var instances = childRecords
                    .Select(r =>
                        {
                            var key = Tuple.Create(domainType, r);
                            return Tuple.Create(r, _instanceDictionary[key]);
                        });

                if(!inheritenceStack.Contains(domainType))
                {
                    instances
                        .ForEach(i => PopulateChildProperties(i.Value2, i.Value1, inheritenceStack));
                }

                if(propertyType.Equals(domainType))
                {
                    return instances.Select(i => i.Value2).FirstOrDefault();
                }
                else
                {
                    return instances.Select(i => i.Value2);
                }
            }
            return null;
        }

        protected Type GetObjectType(PropertyInfo propertyInfo)
        {
            return Reflector.IsEnumerable(propertyInfo.PropertyType) ?
                            propertyInfo.PropertyType.GetGenericArguments().First() :
                            propertyInfo.PropertyType;

        }

        protected object PopulateChildProperties(object instance, IRecord record, List<Type> inheritenceStack)
        {
            var type = instance.GetType();
            inheritenceStack.Add(type);
            var properties = QueryBuilderHelper.GetRelatives(RepositoryMapIndex, type);
            properties.ForEach(p => _accessor.Write(instance, p.RelativeProperty, GetChildPropertyValue(type, p.RelativeType, record, p.RelativeProperty, inheritenceStack)));
            return this;
        }

        protected void PostBinding(object instance, IBindingMap map)
        {
            var tracker = instance as ITrackState;
            if(tracker != null)
            {
                map.MarkPersistedSources(tracker.InstanceState);
                map.MarkTargetKeys(tracker.InstanceState);
                tracker.InstanceState.ClearDirtyState();
                tracker.Deserializing = false;
            }
        }

        public PersistableTypeBinder(IRepositoryMapIndex repositoryMapIndex, DataSet sourceDataSet)
        {
            RepositoryMapIndex = repositoryMapIndex;
            SourceDataSet = sourceDataSet;
            ConstructDataSet(typeof(T), "", sourceDataSet);
        }
    }

    
}